package device

import (
	"bytes"
	"errors"
	"fmt"
	"net"
	"os"
	"sync"
	"time"

	"github.com/alecthomas/log4go"
	"github.com/golang/protobuf/proto"
	"pac/client"
	"pac/log"
	"pac/packet"
	pb "pac/proto"
)

type Client struct {
	Conn             net.Conn
	mu               sync.Mutex
	netAddr          string
	shouldForceClose bool // 是否需要断开
	DeviceID         string
}

func NewClient(conn net.Conn) *Client {
	cli := &Client{
		Conn:    conn,
		netAddr: conn.RemoteAddr().String() + "," + conn.LocalAddr().String(),
	}

	return cli
}

func (c *Client) GetConn() net.Conn {
	return c.Conn
}

func (c *Client) GetType() client.Type {
	return client.Device
}

func (c *Client) GetID() string {
	return c.DeviceID
}

func (c *Client) Close() {
	if c == nil {
		return
	}
	c.mu.Lock()
	if c.Conn != nil {
		err := c.Conn.Close()
		if err != nil {
			log4go.Error(err)
		}
	}
	c.mu.Unlock()

	removed, stillOnline := client.RemoveClient(c)
	if removed && !stillOnline {
		client.NotifToUser(&pb.OnDeviceOnlineStatusReq{
			DeviceId: c.GetID(),
			Online:   false,
		})
	}
}

func (c *Client) Send(data []byte) error {
	conn := c.Conn
	if conn == nil {
		return errors.New("device.Client.Send: connection was closed")
	}
	n, err := conn.Write(data)
	if err != nil {
		return err
	}
	if n != len(data) {
		return fmt.Errorf("device.Client.Send: number of send bytes (%d) not equal request (%d)", n, len(data))
	}
	return nil
}

func (c *Client) SendPacket(pkt packet.Packet) (err error) {
	log.PacketLog(c, true, pkt)
	raw := pkt.GetRaw()
	if len(raw) == 0 {
		if raw, err = pkt.Marshal(); err != nil {
			return err
		}
	}
	return c.Send(raw)
}

func (c *Client) SendReqPb(pb proto.Message) (err error) {
	return nil
}

func (c *Client) ShouldForceClose() bool {
	return c.shouldForceClose
}

func (c *Client) String() string {
	return "[" + c.netAddr + "]" + c.GetID()
}

func HandleConn(conn net.Conn) {
	cli := NewClient(conn)

	defer func() {
		log4go.Info("%v END", cli)
		cli.Close()
	}()

	buf := make([]byte, 1024*8)
	var pendingData bytes.Buffer
	var pkt Packet
	var activity = time.Now()

	var tempDelay time.Duration
	for {
		err := conn.SetReadDeadline(activity.Add(time.Minute))
		if err != nil {
			log4go.Error("%v SetReadDeadline: %v", cli, err)
			return
		}
		n, err := conn.Read(buf)
		if err != nil {
			if os.IsTimeout(err) {
				log4go.Info("%v heart time out (expire: %v)", cli, time.Minute)
				return
			}
			if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				time.Sleep(tempDelay)
				continue
			}
			log4go.Error("%v %v", cli, err)
			return
		}

		//if n > 0 {
		//	log4go.Finest("%v >: %X", cli, buf[:n])
		//}

		pendingData.Write(buf[:n])
		pkt, err = DePacket(&pendingData)
		if err != nil {
			log4go.Error("%v DePacket err: %v", cli, err)
			return
		}
		if pkt == nil {
			continue
		}

		activity = time.Now()

		log.PacketLog(cli, false, pkt)

		if !pkt.GetCmd().Equal(CmdLoginReq) && len(cli.DeviceID) == 0 {
			log4go.Warn("%v should login first", cli)
			return
		}

		processPacket(cli, pkt)

		if cli.ShouldForceClose() {
			log4go.Info("%v ShouldForceClose", cli)
			// 等待发送完成
			time.Sleep(1 * time.Second)
			return
		}
	}
}

func processPacket(cli *Client, pkt Packet) {
	pktParseParamErr := pkt.GetErrOfParseParam()
	if pktParseParamErr != nil {
		log4go.Warn("%v pktParseParamErr: %v", cli, pktParseParamErr)
		if pkt.IsRequest() && pktParseParamErr != nil {
			rsp := &rspPkt{}
			rsp.fromReq(pkt)
			rsp.ErrCode = ErrInvalidParam
			cli.SendPacket(rsp)
		}
		return
	}
	err := pkt.Process(cli)
	if err != nil {
		log4go.Warn("%v process pkt(%v) err: %v", cli, pkt, err)
	}
}
